package com.caigou.service.impl;

import com.alibaba.fastjson.JSON;
import com.caigou.bean.cms.entity.Article;
import com.caigou.bean.cms.entity.ArticleTitle;
import com.caigou.bean.cms.entity.EsArticle;
import com.caigou.bean.cms.param.EsArticleParam;
import com.caigou.bean.vo.KeyValueVO;
import com.caigou.enums.FailResultEnum;
import com.caigou.mapper.cms.ArticleMapper;
import com.caigou.pojo.ArticleList;
import com.caigou.repository.EsArticleRepository;
import com.caigou.service.ArticleService;
import com.caigou.service.IEsArticleService;
import com.caigou.util.Result;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeAction;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeRequestBuilder;
import org.elasticsearch.action.admin.indices.analyze.AnalyzeResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder.Field;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
public class EsArticleServiceImpl implements IEsArticleService {
    @Resource
    private ElasticsearchTemplate elasticsearchTemplate;
    @Resource
    private EsArticleRepository esArticleRepository;
    @Resource
    private ArticleMapper articleMapper;
    @Resource
    private ArticleService articleService;


    @Override
    public EsArticle findEsArticleByArticleId(String articleId) {
        Optional<EsArticle> esArticle = esArticleRepository.findById(articleId);
        return esArticle.orElse(null);
    }


    @Override
    public Result<PageInfo<ArticleList>> findEsArticleListByParam(EsArticleParam param) {


        if (param.getSearchType() == null) {
            return Result.error(FailResultEnum.NO_SEARCH_TYPE);
        }
        if (param.getSearchType() == 1) {
            return Result.success(queryEsArticleTitle(param));
        } else if (param.getSearchType() == 2) {
            return Result.success(queryEsArticleContent(param));
        }
        return null;
    }

    public PageInfo<ArticleList> queryEsArticleContent(EsArticleParam param) {

        PageRequest page = PageRequest.of(param.getPageNum() - 1, param.getPageSize());

        //1.创建QueryBuilder(即设置查询条件)这儿创建的是组合查询(也叫多条件查询),后面会介绍更多的查询方法
        /*组合查询BoolQueryBuilder
         * must(QueryBuilders)   :AND
         * mustNot(QueryBuilders):NOT
         * should:               :OR
         */
        //builder下有must、should以及mustNot 相当于sql中的and、or以及not
        BoolQueryBuilder builder = QueryBuilders.boolQuery();


        //TODO 英文或者拼音搜索暂时不用  后续看业务需求

        if (true) {//包含中文
            //标题汉字搜索
            if (param.getQueryText() != null && !StringUtils.isEmpty(param.getQueryText())) {
                builder.must(QueryBuilders.multiMatchQuery(param.getQueryText(), "articleTitle","articleInfo","searchTitle", "articlePureContent").operator(Operator.OR).boost(1));//boost默认为1
                //builder.must(QueryBuilders.matchQuery("articleTitle", param.getQueryText()).operator(Operator.AND).boost(3));//boost默认为1
                log.info("中文汉字搜索-->" + param.getQueryText());
            }
        }

        //对文章类型搜索
        if (param.getArticleType() != null) {
            if (param.getArticleType() == 0) {
                log.info("未添加搜索条件");
            } else if (param.getArticleType() == 5) {
                builder.must(QueryBuilders.matchQuery("articleType", 14));
            } else {
                builder.must(QueryBuilders.matchQuery("articleType", param.getArticleType()));
            }
        }

        //构造搜索条件

        SortBuilder sort = null;
        if (param.getSort() == 1) {//相似度排序
            sort = SortBuilders.scoreSort().order(SortOrder.DESC);
        } else if (param.getSort() == 2) {//项目时间排序
            sort = SortBuilders.fieldSort("publicTime").order(SortOrder.DESC);
        } else {
            sort = SortBuilders.scoreSort().order(SortOrder.DESC);
        }

        //2.构建查询
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        //将搜索条件设置到构建中
        nativeSearchQueryBuilder.withQuery(builder);
        //将分页设置到构建中
        nativeSearchQueryBuilder.withPageable(page);
        //将排序设置到构建中
        nativeSearchQueryBuilder.withSort(sort);
        //设置高亮显示
        //withHighlightFields(new HighlightBuilder.Field(field))
        HighlightBuilder.Field highlight1 = new HighlightBuilder.Field("articleTitle");
        highlight1.preTags("<span class='high-light'>").postTags("</span>");//红色高亮标签

        HighlightBuilder.Field highlight2 = new HighlightBuilder.Field("articleInfo");
        highlight2.preTags("<span class='high-light'>").postTags("</span>");//红色高亮标签
        //nativeSearchQueryBuilder.withHighlightFields(highlight1, highlight2);
        nativeSearchQueryBuilder.withHighlightFields(highlight1,highlight2);
        //生产NativeSearchQuery
        NativeSearchQuery query = nativeSearchQueryBuilder.build();


        //3.执行方法
        //Page<EsBid> esBidPage = esBidRepository.search(query);
//        Page<EsBid> esBidPage = elasticsearchTemplate.queryForPage(query, EsBid.class);

        //3.高亮显示的执行方法，实施查询，注意：这里的泛型最后和 elasticsearch 中的字段对应
        AggregatedPage<EsArticle> esArticlePage = elasticsearchTemplate.queryForPage(query, EsArticle.class, new SearchResultMapper() {
            @Override
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
                List<T> esBidList = new ArrayList<>();
                //命中记录
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    if (hits.totalHits <= 0) {
                        return null;
                    }

                    Map<String, Object> hitMap = hit.getSourceAsMap();
                    EsArticle esArticle = createEsBid( hitMap);
                    //设置高亮（若对应字段有高亮的话）
                    setHighLight(hit,"articleTitle", esArticle);
                    setHighLight(hit, "articleInfo", esArticle);

                    esBidList.add((T) esArticle);
                }
                return new AggregatedPageImpl<>((List<T>) esBidList, pageable, response.getHits().getTotalHits());

            }
        });

        PageInfo<ArticleList> listPageInfo = esArticlePage2ArticleListPage(esArticlePage, param);

        return listPageInfo;
    }

    private PageInfo<ArticleList> esArticlePage2ArticleListPage(Page<EsArticle> esArticlePage, EsArticleParam param) {


        //将结果转换为pageHelp分页类型结果 便于前台列表字段统一适配
        List<EsArticle> content = esArticlePage.getContent();
        PageInfo<ArticleList> articlePromoterPageInfo = new PageInfo<>();

        List<ArticleList> articleLists = new ArrayList<>();
        if (!CollectionUtils.isEmpty(content)) {
            content.forEach(bean -> {
                ArticleList article = new ArticleList();
                BeanUtils.copyProperties(bean, article);

                //设置文章图片与标题
                List<ArticleTitle> articleTitleVOS = bean.getArticleTitleVOS();
                if (!CollectionUtils.isEmpty(articleTitleVOS)) {
                    articleTitleVOS.stream().filter(articleTitleVO -> articleTitleVO.getArticleTitleType() == -1).findFirst().ifPresent(articleTitleVO -> {
                        article.setArticleImage(articleTitleVO.getArticleImage());
                        article.setArticleImageAlt(articleTitleVO.getArticleImageAlt());
                    });
                }
                article.setArticleReleaseTime(bean.getPublicTime());
                article.setArticleCreateUser(bean.getArticleUserId());
                if (!StringUtils.isEmpty(bean.getArticleKeyWord())) {
                    List<KeyValueVO> collect = bean.getArticleKeyWordVOS().stream().filter(kv -> kv.getLabel() != null).collect(Collectors.toList());
                    article.setArticleKeywords(!CollectionUtils.isEmpty(collect) ? collect : Collections.emptyList());
                }else {
                    article.setArticleKeywords(Collections.emptyList());
                }

                articleLists.add(article);
            });
        }

        articlePromoterPageInfo.setList(articleLists);
        articlePromoterPageInfo.setPageNum(esArticlePage.getNumber() + 1);
        articlePromoterPageInfo.setSize(esArticlePage.getSize());
        articlePromoterPageInfo.setTotal(esArticlePage.getTotalElements());


        articlePromoterPageInfo.setHasNextPage(esArticlePage.isLast());
        articlePromoterPageInfo.setHasPreviousPage(esArticlePage.isFirst());

        articlePromoterPageInfo.setIsFirstPage(param.getPageNum() == 1);
        articlePromoterPageInfo.setIsLastPage(param.getPageNum() <= esArticlePage.getTotalPages());
        articlePromoterPageInfo.setPageSize(param.getPageSize());
        articlePromoterPageInfo.setPageNum(param.getPageNum());
        return articlePromoterPageInfo;
    }

    @Override
    public EsArticle saveEsArticle(EsArticle article) {
        EsArticle save = esArticleRepository.save(article);
        return save;
    }

    @Override
    public List<Article> findUnIndexedArticleList(Integer pageSize) {
        List<Article> articles = articleMapper.findUnIndexedArticleList(pageSize);
        return articles;
    }

    @Override
    public Integer updateArticleIndexStatus(String articleId) {
        return articleMapper.updateArticleIndexStatus(articleId, 1);
    }

    @Override
    public List<Article> findArticleByArticleIds(List<String> articleIds) {

        return articleMapper.findArticleByArticleIdList(articleIds);

    }

    @Override
    public List<String> getIkAnalyzeSearchTerms(String searchContent, int analyzerType) {

        //get调用ik分词器
        AnalyzeRequestBuilder ikRequest = new AnalyzeRequestBuilder(elasticsearchTemplate.getClient(),
                AnalyzeAction.INSTANCE, "book", searchContent);
        //ikRequest.setTokenizer("ik_smart");
        if (analyzerType == 1) {
            ikRequest.setTokenizer("ik_max_word");
        } else if (analyzerType == 2) {
            //ikRequest.setAnalyzer("ik_smart");
            ikRequest.setTokenizer("ik_smart");
        } else {
            ikRequest.setTokenizer("standard");//默认分词器
        }
        //ikRequest.setAnalyzer("ik_smart");
        //ikRequest.setAnalyzer("ik_max_word");
        List<AnalyzeResponse.AnalyzeToken> tokenList = ikRequest.execute().actionGet().getTokens();
        // 循环赋值
        List<String> searchTermList = new ArrayList<>();
        tokenList.forEach(ikToken -> {
            searchTermList.add(ikToken.getTerm());
        });
        String[] array = this.getAnalyzes(searchContent);
        for (int i = 0; i < array.length; i++) {
            log.info("i=" + i + "=" + array[i]);
        }
        return searchTermList;
    }

    @Override
    public void deleteEsArticle(EsArticle bean) {
        esArticleRepository.deleteById(bean.getArticleId());
    }

    /*
     * index 索引index
     * text 需要被分析的词语
     * default 默认使用中文ik_smart分词
     * */
    public String[] getAnalyzes(String text) {
        //调用ES客户端分词器进行分词
        AnalyzeRequestBuilder ikRequest = new AnalyzeRequestBuilder(elasticsearchTemplate.getClient(),
                AnalyzeAction.INSTANCE, "book", text).setAnalyzer("ik_smart");
        List<AnalyzeResponse.AnalyzeToken> ikTokenList = ikRequest.execute().actionGet().getTokens();

        // 赋值
        List<String> searchTermList = new ArrayList<>();
        ikTokenList.forEach(ikToken -> {
            searchTermList.add(ikToken.getTerm());
        });

        return searchTermList.toArray(new String[searchTermList.size()]);
    }

    /**
     * by 通过标讯标题搜索
     *
     * @param
     * @return
     */
    public PageInfo<ArticleList> queryEsArticleTitle(EsArticleParam param) {

        PageRequest page = PageRequest.of(param.getPageNum() - 1, param.getPageSize());

        //1.创建QueryBuilder(即设置查询条件)这儿创建的是组合查询(也叫多条件查询),后面会介绍更多的查询方法
        /*组合查询BoolQueryBuilder
         * must(QueryBuilders)   :AND
         * mustNot(QueryBuilders):NOT
         * should:               :OR
         */
        //builder下有must、should以及mustNot 相当于sql中的and、or以及not
        BoolQueryBuilder builder = QueryBuilders.boolQuery();

        //设置精确搜索,标讯信息中的title含有text
//        BoolQueryBuilder titleBuilder = QueryBuilders.boolQuery();
//        titleBuilder.should(QueryBuilders.matchQuery("title", param.getQueryText()).boost(3))
//                    .should(QueryBuilders.matchQuery("title.ik", param.getQueryText()).boost(2))
//                    .should(QueryBuilders.matchQuery("title.pinyin", param.getQueryText()).boost(2));
//        builder.must(titleBuilder);//boost默认为1

//        BoolQueryBuilder titleBuilder = QueryBuilders.boolQuery();
//        titleBuilder.should(QueryBuilders.matchQuery("title", param.getQueryText()).operator(Operator.AND).boost(3))
//                    .should(QueryBuilders.matchQuery("titlePinYin", param.getQueryText()).operator(Operator.AND));
//        builder.must(titleBuilder);

        //是否包含中文的正则表达式判断
        //TODO 英文或者拼音搜索暂时不用  后续看业务需求
        //Pattern pattern = Pattern.compile("[\u4E00-\u9FA5]");
        //Matcher matcher = pattern.matcher(param.getQueryText());
        //if (matcher.find()) {//包含中文
        if (true) {//包含中文
            //标题汉字搜索
            if (param.getQueryText() != null && !StringUtils.isEmpty(param.getQueryText())) {
                builder.must(QueryBuilders.matchQuery("articleTitle", param.getQueryText()).operator(Operator.AND).boost(3));//boost默认为1
                log.info("中文汉字搜索-->" + param.getQueryText());
            }

        } else {//纯拼音或者英文
            if (param.getQueryText() != null && !StringUtils.isEmpty(param.getQueryText())) {
                BoolQueryBuilder titleBuilder = QueryBuilders.boolQuery();
                titleBuilder.should(QueryBuilders.matchQuery("articleTitle", param.getQueryText()).operator(Operator.AND).boost(3))
                        .should(QueryBuilders.matchQuery("titlePinYin", param.getQueryText()).operator(Operator.AND));
                builder.must(titleBuilder);
                log.info("中文拼音搜索-->" + param.getQueryText());
            }

        }

        log.info("中文汉字搜索-->" + param.getQueryText());
//        //标题汉字搜索
//        if (param.getQueryText() != null && !StringUtils.isEmpty(param.getQueryText())) {
//            builder.must(QueryBuilders.matchQuery("searchTitle", param.getQueryText()).operator(Operator.AND).boost(3));//boost默认为1
//        }

        //拼音搜索
        //builder.must(QueryBuilders.matchQuery("titlePinYin", param.getQueryText()).operator(Operator.AND).boost(2));
        //builder.must(QueryBuilders.matchQuery("title", param.getQueryText()).operator(Operator.AND).boost(3));//operator默认为or
//        //设置精确搜索，标讯信息的projectName精确匹配text,，也可以模糊匹配QueryBuilders.fuzzyQuery(name, value)
//        builder.must(QueryBuilders.matchQuery("projectName", param.getQueryText()));

        //可以同时设置多个字段的搜索，第一个参数是查询的值，后面的参数是字段名，可以跟多个字段，用逗号隔开
//        builder.must(QueryBuilders.multiMatchQuery(param.getQueryText(), "title", "projectName"));

        //设置精确搜索，对标讯简介内容
        //builder.must(QueryBuilders.matchQuery("projectInfo", param.getQueryText()));
        //builder.must(QueryBuilders.matchQuery("projectInfo", param.getQueryText()).operator(Operator.AND));

        //对品类进行精确搜索
        //builder.must(QueryBuilders.matchQuery("item", param.getQueryText())).boost(2);
 
        //对文章类型搜索
        if (param.getArticleType() != null) {
            if (param.getArticleType() == 0) {
                log.info("未添加搜索条件");
            } else if (param.getArticleType() == 5) {
                builder.must(QueryBuilders.matchQuery("articleType", 14));
            } else {
                builder.must(QueryBuilders.matchQuery("articleType", param.getArticleType()));
            }
        }

        //构造搜索条件

        SortBuilder sort = null;
        if (param.getSort() == 1) {//相似度排序
            sort = SortBuilders.scoreSort().order(SortOrder.DESC);
        } else if (param.getSort() == 2) {//项目时间排序
            sort = SortBuilders.fieldSort("publicTime").order(SortOrder.DESC);
        } else {
            sort = SortBuilders.scoreSort().order(SortOrder.DESC);
        }
        //2.构建查询
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        //将搜索条件设置到构建中
        nativeSearchQueryBuilder.withQuery(builder);
        //将分页设置到构建中
        nativeSearchQueryBuilder.withPageable(page);
        //将排序设置到构建中
        //nativeSearchQueryBuilder.withSort(sort).withSort(sort2);
        nativeSearchQueryBuilder.withSort(sort);
        //设置高亮显示
        //withHighlightFields(new HighlightBuilder.Field(field))

        HighlightBuilder.Field highlight1 = new HighlightBuilder.Field("articleTitle");
        highlight1.preTags("<span class='high-light'>").postTags("</span>");//红色高亮标签
        //Field highlight2 = new HighlightBuilder.Field("projectName");
        //nativeSearchQueryBuilder.withHighlightFields(highlight1, highlight2);
        nativeSearchQueryBuilder.withHighlightFields(highlight1);
        //设置返回字段
//        String[] includes = null;//{"title", "projectName"};
        String[] excludes = {"articlePureContent"};
        FetchSourceFilter fetchSourceFilter = new FetchSourceFilter(null, excludes);
        nativeSearchQueryBuilder.withSourceFilter(fetchSourceFilter);
//        nativeSearchQueryBuilder.withFields("title");
        //生成NativeSearchQuery
        NativeSearchQuery query = nativeSearchQueryBuilder.build();

//        //es搜索指定返回字段
//        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//        searchSourceBuilder.fetchSource(new String[]{"title", "projectDate"}, new String[]{"pureContent", "content"});

        //3.执行方法
        //Page<EsBid> esBidPage = esBidRepository.search(query);
//        Page<EsBid> esBidPage = elasticsearchTemplate.queryForPage(query, EsBid.class);

        //3.高亮显示的执行方法，实施查询，注意：这里的泛型最后和 elasticsearch 中的字段对应
        AggregatedPage<EsArticle> esBidPage = elasticsearchTemplate.queryForPage(query, EsArticle.class, new SearchResultMapper() {
            @Override
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
                List<T> esBidList = new ArrayList<>();
                //命中记录
                SearchHits hits = response.getHits();
                for (SearchHit hit : hits) {
                    if (hits.totalHits <= 0) {
                        return null;
                    }

                    Map<String, Object> hitMap = hit.getSourceAsMap();
                    EsArticle esBid = createEsBid( hitMap);

                    //设置高亮（若对应字段有高亮的话）
                    setHighLight(hit,"articleTitle", esBid);
                    //setHighLight(hit, "projectName", esBid);

                    esBidList.add((T) esBid);
                }
                return new AggregatedPageImpl<>((List<T>) esBidList, pageable, response.getHits().getTotalHits());

            }
        });

        PageInfo<ArticleList> listPageInfo = esArticlePage2ArticleListPage(esBidPage, param);

        return listPageInfo;
    }

    private EsArticle createEsBid(Map<String, Object> hitMap) {
        EsArticle esArticle = new EsArticle();
        EsArticle article = JSON.parseObject(JSON.toJSONString(hitMap), EsArticle.class);
        BeanUtils.copyProperties(article,esArticle);
        //esArticle.setArticleTitleVOS(null);
        return esArticle;
    }

    /**
     * set设置高亮
     *  @param
     * @param hit    命中记录
     * @param object 待赋值对象
     */
    private static void setHighLight(SearchHit hit,String field, Object object) {
        //获取对应的高亮域
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        HighlightField highlightField = highlightFields.get(field);
        if (highlightField != null) {
            //取得定义的高亮标签
            String highLightMessage = highlightField.fragments()[0].toString();
            // 反射调用set方法将高亮内容设置进去
            try {
                String setMethodName = parseSetMethodName(field);
                Class<?> Clazz = object.getClass();
                Method setMethod = Clazz.getMethod(setMethodName, String.class);
                setMethod.invoke(object, highLightMessage);
            } catch (Exception e) {
                log.error("反射报错", e);
            }
        }
    }

    /**
     * parsetMethodName 根据字段名，获取Set方法名
     *
     * @param fieldName 字段名
     * @return Set方法名
     */
    private static String parseSetMethodName(String fieldName) {
        if (StringUtils.isEmpty(fieldName)) {
            return null;
        }
        int startIndex = 0;
        if (fieldName.charAt(0) == '_') {
            startIndex = 1;
        }
        return "set" + fieldName.substring(startIndex, startIndex + 1).toUpperCase()
                + fieldName.substring(startIndex + 1);
    }
}
